百度APP iOS端包体积50M优化实践(五) HEIC图片和无用类优化实践
The following article is from 百度App技术 Author TXT
一、前言
GEEK TALK
之前的文章介绍了图片优化和代码优化的几种方式,本篇文章重点介绍HEIC图片和无用类检测的优化实践。HEIC是High Efficiency Image Format(高效图像格式)的缩写,是一种新的图像文件格式,它是2017年苹果公司在iOS 11中引入,用于代替JPEG图像格式,以更高效地压缩图像并减少存储空间占用。HEIC支持多帧图像、透明度和16位深度色彩,使得它成为高质量图像和动画的理想选择。本文重点探究HEIC图片在百度APP中使用的可行性和包体积收益,验证HEIC图片在Bundle和Asset Catalog的兼容性,重点研究了Asset Catalog管理图片的机制,记录了验证过程中发现的特殊问题和解决思路。无用类则是详细介绍了如何用静态分析和动态分析相结合的方式,精简代码体积。
百度APP iOS端包体积优化实践系列文章回顾:
《百度APP iOS端包体积50M优化实践(二) 图片优化》
测试环境 | |
二、 HEIC图片格式转换和使用方式
GEEK TALK
2.1 格式转换
有三种常见的HEIC图片转换方式:Mac图片转换功能、Mac自带sips命令、多平台支持的ImageMagick命令。
2.1.1 Mac图片转换功能:
右键图片,快速操作—>转换图像 格式选HEIF,图像大小根据需求选择
举例:sips -s format heic -s formatOptions default guideview@3x.png --out guideview@3x.heic
2.1.3 ImageMagick工具:
brew install imagemagick convert guideview@3x.png guideview@3x.heic
2.2 HEIC在iOS中使用
// 加载本地图片
UIImage *image = [UIImage imageNamed:@"heifFileName"];
UIImage *image = [UIImage imageWithContentsOfFile:filePath];
// 由 网络请求的 NSData 解码
UIImage *image = [UIImage imageWithData:heifImageData];
2.3 HEIC图片兼容性
可以调用ImageI/O相关函数获取支持的图片编解码支持的格式,这里值得注意的是,在iPhone6p,iOS11.0.4实测不支持HEIC,即调用CGImageSourceCopyTypeIdentifiers();查询可解码格式包含public.heic,依旧是无法正常显示出HEIC图片;在iPhone6p,iOS12.5.6测试机上可以正常显示HEIC图片。
//获取所支持的图片格式数组,解码
CFArrayRef decodeArr = CGImageSourceCopyTypeIdentifiers();
NSArray *decodeUTI = (__bridge NSArray *)decodeArr;
NSLog(@"解码支持%@", decodeUTI);
//获取所支持的图片格式数组,编码
CFArrayRef encodeArr = CGImageDestinationCopyTypeIdentifiers();
NSArray *encodeUTI = (__bridge NSArray *)encodeArr;
NSLog(@"编码支持%@", encodeUTI);
三、 Bundle和Asset Catalog的兼容性
GEEK TALK
在iOS系统中,APP内的图片资源可以放Bundle和Asset Catalog。若图片放Bundle中,ipa包安装到设备上后,图片占用的磁盘空间和图片实际大小一致。不过放Bundle的缺点是需要针对不同分辨率的进行放不同大小的倍图,明显增加了包体积。
苹果推荐使用Asset Catalog管理内置资源,包括图片资源、音视频等,同样也支持HIEC图片。Asset Catalog的好处显然易见,支持app slicing、支持设置拉伸区域、给不同的机型配置不同的图片、配置渲染颜色等。最终所有的文件最终会打包成.car压缩文件。
对此,我们选择了两张具有代表性的图片,log.png是带有Alpha通道的图片和guideview@3x.png是不带Alpha通道的图片。然后分别生成对应的HEIC图片log.heic和guideview@3x.heic,图片没有经过任何其余压缩处理。
△guideview@3x.png
△log.png
3.1 生成car文件
// Any iOS Device(arm64)
/Applications/Xcode.app/Contents/Developer/usr/bin/actool --output-format human-readable-text --notices --warnings --export-dependency-info /Users/xxxxx/Library/Developer/Xcode/DerivedData/ImageDemoS-auwsocxqgbwbgmfoiguzuahzizre/Build/Intermediates.noindex/ImageDemoS.build/Debug-iphoneos/ImageDemoS.build/assetcatalog_dependencies --output-partial-info-plist /Users/xxxxx/Library/Developer/Xcode/DerivedData/ImageDemoS-auwsocxqgbwbgmfoiguzuahzizre/Build/Intermediates.noindex/ImageDemoS.build/Debug-iphoneos/ImageDemoS.build/assetcatalog_generated_info.plist --app-icon AppIcon --accent-color AccentColor --compress-pngs --enable-on-demand-resources YES --development-region en --target-device iphone --target-device ipad --minimum-deployment-target 16.0 --platform iphoneos --compile /Users/xxxxx/Library/Developer/Xcode/DerivedData/ImageDemoS-auwsocxqgbwbgmfoiguzuahzizre/Build/Products/Debug-iphoneos/ImageDemoS.app /Users/xxxxxR/baidu/personal-code/ImageDemoS/ImageDemoS/Assets.xcassets /Users/xxxxx/baidu/personal-code/ImageDemoS/Media.xcassets
// iPhone6p(iOS11.4.1)
/Applications/Xcode.app/Contents/Developer/usr/bin/actool --output-format human-readable-text --notices --warnings --export-dependency-info /Users/xxxxx/Library/Developer/Xcode/DerivedData/ImageDemoS-auwsocxqgbwbgmfoiguzuahzizre/Build/Intermediates.noindex/ImageDemoS.build/Debug-iphoneos/ImageDemoS.build/assetcatalog_dependencies --output-partial-info-plist /Users/xxxxx/Library/Developer/Xcode/DerivedData/ImageDemoS-auwsocxqgbwbgmfoiguzuahzizre/Build/Intermediates.noindex/ImageDemoS.build/Debug-iphoneos/ImageDemoS.build/assetcatalog_generated_info.plist --app-icon AppIcon --accent-color AccentColor --compress-pngs --enable-on-demand-resources YES --optimization space --filter-for-thinning-device-configuration iPhone7,1 --filter-for-device-os-version 11.4.1 --development-region en --target-device iphone --target-device ipad --minimum-deployment-target 9.0 --platform iphoneos --compile /Users/xxxxx/Library/Developer/Xcode/DerivedData/ImageDemoS-auwsocxqgbwbgmfoiguzuahzizre/Build/Products/Debug-iphoneos/ImageDemoS.app /Users/xxxxx/baidu/personal-code/ImageDemoS/ImageDemoS/Assets.xcassets /Users/xxxxx/baidu/personal-code/ImageDemoS/Media.xcassets
/* com.apple.actool.document.warnings */
/Media.xcassets:./logHEICAlpha.imageset/[universal][][][3x][][][][][][][][][][]: warning: You're targeting iOS 9.0, but HEIF files can only be accessed from an Asset Catalog in iOS 11.0 and later.
3.2 解析car文件
assetutil -I Assets.car > Assets.json
SizeOnDisk:这是图片在Assets.car里实际的体积
Encoding:编码方式,HEIF就是HEIC图片的编码方式
Compression:压缩算法
1、PNG图片转HEIC图片体积会有所下降;
2、PNG图片和HEIC图片经过actool处理后,car文件里的图片大小和实际大小不一致;
3、car文件里图片大小包体积和编码方式和压缩算法相关,PNG和HEIC图片的最终大小以SizeOnDisk字段数据为准;
4、actool会对图片做iOS系统和设备兼容,在不支持HEIC的设备上会将HEIC图片转为其他可以显示的格式。
PNG图片 | { "BitsPerComponent" : 8, "ColorModel" : "RGB", "Colorspace" : "srgb", "Compression" : "deepmap2", "Encoding" : "ARGB", "Name" : "logPNGAlpha", "NameIdentifier" : 62170, "Opaque" : false, "PixelHeight" : 258, "PixelWidth" : 540, "RenditionName" : "log.png", "Scale" : 3, "SizeOnDisk" : 9441, "Template Mode" : "automatic" } |
(支持HEIC的设备) | { "AssetType" : "Image", "BitsPerComponent" : 8, "ColorModel" : "RGB", "DeploymentTarget" : "2017", "Encoding" : "HEIF", "Name" : "logHEICAlpha", "NameIdentifier" : 2469, "Opaque" : false, "PixelHeight" : 258, "PixelWidth" : 540, "RenditionName" : "log.heic", "Scale" : 3, "SizeOnDisk" : 11239, "Template Mode" : "automatic" } |
(支持HEIC的设备) | { "AssetType" : "Image", "BitsPerComponent" : 8, "ColorModel" : "RGB", "Colorspace" : "srgb", "Compression" : "jpeg-lzfse", "Encoding" : "ARGB", "Name" : "logHEICAlpha", "NameIdentifier" : 2469, "Opaque" : false, "PixelHeight" : 258, "PixelWidth" : 540, "RenditionName" : "log.heic", "Scale" : 3, "SizeOnDisk" : 38227, "Template Mode" : "automatic" } |
图片大小:72,547(guideview@3x.png)33,589(guideview@3x.heic)
(支持HEIC的设备) | |
(支持HEIC的设备) |
四、Alpha通道兼容性问题
GEEK TALK
△图片显示为白色
△透明显示为绿色
4.1 问题分析思路
1、为什么同一张HIEC图片,iOS14和iOS15显示正常,而iOS11、iOS12、iOS13会出现问题?
2、为什么同样是带Alpha通道的HEIC图片,有的会在iOS11、iOS12、iOS13系统上出现问题,有的图片在所有系统都可以正常显示?
UIKit提供放UIImage的CGImage属性,这是最常用的方式; ImageI/O提供的CGImageSourceCreateImageAtIndex 函数,这种适用于从文件解析图片; Core graphics提供的CGBitmapContextCreateImage,这种适用于已知bitmap graphics context情况下使用;
/// 获取图片信息和像素
/// - Parameters:
/// - image: <#image description#>
-(void)dumpImageInfo:(UIImage *)image
{
// 获取CGImageRef
CGImageRef cgimage = image.CGImage;
size_t width = CGImageGetWidth(cgimage);
size_t height = CGImageGetHeight(cgimage);
size_t bpr = CGImageGetBytesPerRow(cgimage);
size_t bpp = CGImageGetBitsPerPixel(cgimage);
size_t bpc = CGImageGetBitsPerComponent(cgimage);
size_t bytes_per_pixel = bpp / bpc;
CGBitmapInfo info = CGImageGetBitmapInfo(cgimage);
NSLog(
@"\n"
// "===== %@ =====\n"
"CGImageGetHeight: %d\n"
"CGImageGetWidth: %d\n"
"CGImageGetColorSpace: %@\n"
"CGImageGetBitsPerPixel: %d\n"
"CGImageGetBitsPerComponent: %d\n"
"CGImageGetBytesPerRow: %d\n"
"CGImageGetBitmapInfo: 0x%.8X\n"
" kCGBitmapAlphaInfoMask = %s\n"
" kCGBitmapFloatComponents = %s\n"
" kCGBitmapByteOrderMask = %s\n"
" kCGBitmapByteOrderDefault = %s\n"
" kCGBitmapByteOrder16Little = %s\n"
" kCGBitmapByteOrder32Little = %s\n"
" kCGBitmapByteOrder16Big = %s\n"
" kCGBitmapByteOrder32Big = %s\n",
// file,
(int)width,
(int)height,
CGImageGetColorSpace(cgimage),
(int)bpp,
(int)bpc,
(int)bpr,
(unsigned)info,
(info & kCGBitmapAlphaInfoMask) ? "YES" : "NO",
(info & kCGBitmapFloatComponents) ? "YES" : "NO",
(info & kCGBitmapByteOrderMask) ? "YES" : "NO",
(info & kCGBitmapByteOrderDefault) ? "YES" : "NO",
(info & kCGBitmapByteOrder16Little) ? "YES" : "NO",
(info & kCGBitmapByteOrder32Little) ? "YES" : "NO",
(info & kCGBitmapByteOrder16Big) ? "YES" : "NO",
(info & kCGBitmapByteOrder32Big) ? "YES" : "NO"
);
// 获取位图数据
CGDataProviderRef provider = CGImageGetDataProvider(cgimage);
NSData* data = (__bridge NSData *)CGDataProviderCopyData(provider);
// [data autorelease];
const uint8_t* bytes = [data bytes];
printf("Pixel Data:\n");
for(size_t row = 0; row < height; row++)
{
for(size_t col = 0; col < width; col++)
{
const uint8_t* pixel =
&bytes[row * bpr + col * bytes_per_pixel];
printf("(");
for(size_t x = 0; x < bytes_per_pixel; x++)
{
printf("%.2d", pixel[x]);
if( x < bytes_per_pixel - 1 )
printf(",");
}
printf(")");
if( col < width - 1 )
printf(", ");
}
printf("\n");
}
}
/// 从支持解码的图片创建CGImageRef
/// - Parameter path: 图片路径
CGImageRef createCGImageFromFile (NSString* path)
{
// Get the URL for the pathname passed to the function.
NSURL *url = [NSURL fileURLWithPath:path];
CGImageRef myImage = NULL;
CGImageSourceRef myImageSource;
CFDictionaryRef myOptions = NULL;
CFStringRef myKeys[2];
CFTypeRef myValues[2];
// Set up options if you want them. The options here are for
// caching the image in a decoded form and for using floating-point
// values if the image format supports them.
myKeys[0] = kCGImageSourceShouldCache;
myValues[0] = (CFTypeRef)kCFBooleanTrue;
myKeys[1] = kCGImageSourceShouldAllowFloat;
myValues[1] = (CFTypeRef)kCFBooleanTrue;
// Create the dictionary
myOptions = CFDictionaryCreate(NULL, (const void **) myKeys,
(const void **) myValues, 2,
&kCFTypeDictionaryKeyCallBacks,
& kCFTypeDictionaryValueCallBacks);
// Create an image source from the URL.
myImageSource = CGImageSourceCreateWithURL((CFURLRef)url, myOptions);
CFRelease(myOptions);
// Make sure the image source exists before continuing
if (myImageSource == NULL){
fprintf(stderr, "Image source is NULL.");
return NULL;
}
// Create an image from the first item in the image source.
myImage = CGImageSourceCreateImageAtIndex(myImageSource, 0, NULL);
CFRelease(myImageSource);
// Make sure the image exists before continuing
if (myImage == NULL){
fprintf(stderr, "Image not created from image source.");
return NULL;
}
return myImage;
}
/// 将任意一种格式的图片由UIImage编码为HEIC图片存储
/// - Parameters:
/// - image: <#image description#>
/// - path: <#path description#>
- (void)generateNewHEIC:(UIImage *)image savePath:(NSString *)path{
NSMutableData *imageData = [NSMutableData data];
// HEIC图片编码格式
CFStringRef imageUTType = CFSTR("public.heic");
CGImageDestinationRef destination = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)imageData, imageUTType, 1, NULL);
if (!destination) {
// 无法编码,基本上是因为目标格式不支持
NSLog(@"无法编码");
return;
}
CGImageRef imageRef = image.CGImage; // 待编码的CGImage
// 可选元信息,比如EXIF方向
CGImagePropertyOrientation exifOrientation = kCGImagePropertyOrientationDown;
NSMutableDictionary *frameProperties = [NSMutableDictionary dictionary];
// imageProperties[(__bridge_transfer NSString *) kCGImagePropertyExifDictionary] = @(exifOrientation);
// 添加图像和元信息
CGImageDestinationAddImage(destination, imageRef, (__bridge CFDictionaryRef)frameProperties);
if (CGImageDestinationFinalize(destination) == NO) {
// 编码失败
imageData = nil;
}
// 编码成功,清理……
CFRelease(destination);
// 保存新生成的HEIC图片
if(imageData) {
NSURL *url = [NSURL fileURLWithPath:path];
[imageData writeToURL:url atomically:YES];
}
}
五、图片最佳实践
GEEK TALK
1、修改拦截阈值,大资源和大图片准入拦截阈值从50KB降低至20KB;
2、增加图片优化提示功能,提交新图片时自动对图片进行压缩和格式转换,给出图片最佳大小建议。
5.1 方案
1、在执行git commit时,检测提交的文件,如果为非代码文件,检测文件大小。大资源和大图片准入拦截阈值从50KB降低至20KB;
2、计算各种优化后的图图片在Bundle和Asset Catalog里的大小,计算出最佳的优化方式,文字提示RD优化,不会拦截提交
3、图片优化方式分为两类,一类是存放位置,放Bundle和放Asset Catalog;另一类是对图片进行处理,有压缩和转换格式两种处理方式,两种相互结合得到最佳方式:
放Bundle:不推荐,图片在安装包内体积为图片本身大小,Xcode不会处理,在iOS11以下系统中无法兼容HEIC图片;
放Asset Catalog:推荐,Xcode打包编译时会用actool工具处理图片,优化图片大小,可以兼容HEIC图片;
pngquant压缩:有损压缩PNG图片,沿用百度APP之前的图片压缩参数,是git hook原有逻辑;
MozJPEG压缩:有损压缩JPG图片,新增的压缩工具,https://calendar.perfplanet.com/2014/mozjpeg-3-0/;
HEIC图片:无损转换,百度APP中只能在Asset Catalog中使用,需要回归iOS11系统是否正常显示;
△图片提交检测
六、检测无用类
GEEK TALK
6.1 无用类检测原理分析
△无用类分析与分发
七、静态分析
GEEK TALK
7.1 解析Mach-O
% file -b BaiduBoxApp.app/BaiduBoxApp #获取Mach-O架构
Mach-O 64-bit executable arm64
% otool -arch arm64 -oV BaiduBoxApp.app/BaiduBoxApp > ovrelease.txt #解析Mach-O内容
'Contents of (__DATA,__objc_classlist) section', # classlist节标识
'Contents of (__DATA,__objc_classrefs) section', # classrefs节标
'Contents of (__DATA,__objc_superrefs) section', # 父类节标
'Contents of (__DATA,__objc_catlist) section', # category节标
'Contents of (__DATA,__objc_protolist) section',
'Contents of (__DATA,__objc_selrefs) section',
'Contents of (__DATA,__objc_imageinfo) section'
△debug包
△release包
7.2 注意点
1、在实际分析的过程中发现,如果一个类的子类被实例化,父类未被实例化,此时父类不会出现在__objc_classrefs这个段里,在未使用的类中需要将这一部分父类过滤出去。
2、多个类中可能存在相同的方法名。因为MachO文件中__cstring和__objc_methname这两个代码段记录的是方法名字符的ASCII码的十六进制表示。如果多个类中有相同的方法名,相同的方法名会进入link map的Dead Stripped Symbols中,最后只留一个。
3、如果做了段迁移,可能导致otool工具无法解析对应方法名,但是通过符号地址我们可以在linkmap里还原出具体符号。
△Symbol解析
八、动态分析
GEEK TALK
8.1 动态分析原理
// class is initialized
#define RW_INITIALIZED (1<<29)
struct objc_class : objc_object {
bool isInitialized() {
return getMeta()->data()->flags & RW_INITIALIZED;
}
};
8.2 技术实现
#define RW_INITIALIZED (1<<29)
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# endif
struct lazyFake_objc_class : lazyFake_objc_object {
//提供metaClass函数,获取元类对象
lazyFake_objc_class* metaClass() {
#if __LP64__
//isa指针需要经过一次 &ISA_MASK操作之后才得到真正的地址
return (lazyFake_objc_class *)((long long)isa & ISA_MASK);
#else
return (lazyFake_objc_class *)((long long)isa);
#endif
}
bool isInitialized() {
return metaClass()->data()->flags & RW_INITIALIZED;
}
};
Dl_info info;
dladdr(&_mh_execute_header, &info);
classes = objc_copyClassNamesForImage(info.dli_fname, &classCount);
struct lazyFake_objc_class *objectClass = (__bridge struct lazyFake_objc_class *)cls;
BOOL isInitial = objectClass->isInitialized();
九、 总结
GEEK TALK
1、HEIC图片相较于PNG,对部分图片可以降低图片体积,收益从10%-70%不等,具体问题具体分析,编写git hook检查脚本提供指导;
2、HEIC图片放Asset Catalog可以兼容iOS10以上的所有机型和系统;
3、HEIC图片放Bundle只能在iOS12系统上解码,这个和Apple给出的结论相悖。若APP最低支持系统小于iOS12,则HEIC图片禁止放Bundle。A9以上芯片的机型为硬解,速度更快;
4、带有Alpha通道的PNG图片,未经过pngquant有损压缩的,利用sips命令直接转HEIC图片可以正常显示;
5、带有Alpha通道的PNG图片,已经被pngquant有损压缩过的在iOS12,13,14系统上会显示绿幕,iOS115,iOS16显示正常。虽然显示正常,但是RGB位图颜色解码错误,只是因为alpha为0,绿色变成了透明;
6、无论是PNG还是HEIC图片,在Asset Catalog管理下,打包生成的体积和原图片不同,都会经过不同的处理压缩,可能变大也可能变小,以最终产物为准;
7、pngquant适合对Bundle里的PNG压缩,获取收益,对Asset Catalog里的图片不应该处理,因为这个收益其实是有损压缩获取的,并且会导致压缩过的带Alpha通道的PNG无法转HEIC;
8、无用类检测结合动态检测和静态检测,检测较为严格,主要是为了降低误报率,降低对RD的影响,实际操作过程中发现有些无用类会被漏检。准确度和覆盖度需要根据需求动态调整。
END
参考文献:
推荐阅读: